// 
//  Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 

using System;
using System.Collections.Generic;

using Anculus.Core;

using Galaxium.Core;

using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Extensions.Disco;
using Galaxium.Protocol.Xmpp.Library.Core;

// TODO: registration, moderator/admin/owner use cases

namespace Galaxium.Protocol.Xmpp.Library.Messaging
{
	public enum RoomAffiliation { None, Outcast, Member, Admin, Owner }
	public enum RoomRole { None, Visitor, Participant, Moderator }

	public class Conference: IMessageListener, IPresenceListener
	{
		private enum StatusCode {
			NonAnonymous = 100, AffiliationChanged, ShowsUnavailable, DoesntShowUnavailable,
			ConfigurationChanged, OwnPresence = 110, LoggingEnabled = 170, LoggingDisabled,
			BecameNonAnonymous, BecameSemiAnonymous, BecameFullyAnonymous, RoomCreated = 201,
			NickAssigned = 210,	Banned = 301, NicknameChanged = 303, Kicked = 307,
			AffiliationRemovalReason = 321, RemovedAsNonMember = 322, SystemShutdown = 332
		}
		
		internal Conference (Client client, JabberID jid)
		{
			if (jid.IsFull)
				throw new ArgumentException ("JID must be bare.");
			
			_client = client;
			UID = jid;
		}
		
		public event EventHandler<JoinFailedEventArgs> JoinFailed;
		
		public event EventHandler<JoinEventArgs> Joined;
		
		public event EventHandler<NicknameRequestEventArgs> NicknameRequested;
		public event EventHandler<PasswordRequestEventArgs> PasswordRequested;
		
		public event EventHandler<MucExitEventArgs> Exited;
		
		public event EventHandler<MucMessageEventArgs> MessageReceived;
		public event EventHandler<MucErrorEventArgs> ErrorReceived;
		
		public event EventHandler<MucSubjectEventArgs> SubjectChanged;
		
		public event EventHandler<RoomContactEventArgs> ContactAdded;
		public event EventHandler<RoomContactEventArgs> ContactRemoved;
		public event EventHandler<RoomContactEventArgs> ContactStateChanged;
		public event EventHandler<RoomContactNicknameEventArgs> ContactNicknameChanged;
		public event EventHandler<RoomContactEventArgs> ContactPresenceChanged;
		public event EventHandler<RoomContactEventArgs> ContactAffiliationChanged;
		public event EventHandler<RoomContactEventArgs> ContactRoleChanged;
			
		private Client _client;

		private string _nickname;
		
		private bool _joining;
		
		private RoomContact _last_received;                   // HACK for ejabberd
		private List<StatusCode> _last_status_codes;          //
		
		private IEnumerable<HistoryRestriction> _restrictions;
		
		public JabberID UID { get; private set; }
		
		public bool Connected { get; private set; }
		
		private Dictionary<string, RoomContact> _contacts =
			new Dictionary<string, RoomContact> ();
		
		public void Join (string nickname, string password, IEnumerable<HistoryRestriction> restrictions)
		{
			_client.AttachMessageListener (this, 50);
			_client.AttachPresenceListener (this, 50);
						
			_restrictions = restrictions;
			
			_nickname = GetRegisteredNickname () ?? nickname ?? RequestNickname ();
			
			if (_nickname == null) {
				OnJoinFailed (JoinFailureReason.Cancelled, null);
				return;
			}
			
			var jid = new JabberID (UID.Node, UID.Domain, _nickname);
			var presence = new Presence (PresType.Available, jid);
			var x = new Element ("x", Namespaces.Muc);
			
			if (!String.IsNullOrEmpty (password))
				x.AppendTextChild ("password", null, password);
			
			if (restrictions != null) {
				foreach (var restriction in restrictions)
					x.AppendChild (restriction.ToXml ());
			}
						
			presence.AppendChild (x);
			
			_joining = true;
			_client.Send (presence);
		}
		
		public void ChangeNickname (string nickname)
		{
			var jid = new JabberID (UID.Node, UID.Domain, nickname);
			var presence = new Presence (PresType.Available, jid);
			_client.Send (presence);
		}
		
		public void Leave (string reason)
		{
			if (!Connected) return;
			var jid = new JabberID (UID.Node, UID.Domain, _nickname);
			var presence = new Presence (PresType.Unavailable, jid);
			presence.AppendStatus (reason);
			_client.Send (presence);
		}

		bool IMessageListener.HandleMessage (XmlMessage msg)
		{
			if (msg.From.Bare () != UID) return false;
			
			if (msg.IsError)
				OnErrorReceived (msg.Error);
			else
				HandleChatMessage (msg);
			
			return true;
		}
		
		public JabberID GetUID (string nick)
		{
			if (nick == null) return null;
			RoomContact contact;
			if (_contacts.TryGetValue (nick, out contact)) return contact.UID;
			return null;
		}
		
		private void HandleChatMessage (XmlMessage msg)
		{
			if (_joining && _last_received != null) { // HACK for ejabberd
				_last_received.IsSelfContact = true;
				var non_anonymous = _last_status_codes.BinarySearch (StatusCode.NonAnonymous) >= 0;
				var assigned = _last_status_codes.BinarySearch (StatusCode.NickAssigned) >= 0;
				var logged = _last_status_codes.BinarySearch (StatusCode.LoggingEnabled) >= 0;
				var created = _last_status_codes.BinarySearch (StatusCode.RoomCreated) >= 0;
				_last_status_codes = null;
								
				OnJoined (_last_received.Nickname, assigned, logged, non_anonymous, created);
				_last_received = null;
			}
			
			var nick = msg.From.Resource;
			DateTime timestamp; JabberID source; string reason;
			var delayed = msg.GetDelay (out timestamp, out source, out reason);
			var uid = delayed ? source : GetUID (nick);
			if (!delayed) timestamp = msg.Received;
			
			if (msg.Subject != null)
				OnSubjectChanged (nick, uid, timestamp, msg.Subject);
			else
				OnMessageReceived (nick, uid, timestamp, msg.Thread,
				                   msg.Body, delayed, msg.Type != MsgType.GroupChat);
		}
		
		bool IPresenceListener.HandlePresence (Presence pres)
		{
			if (pres.From.Bare () != UID) return false;
			
			switch (pres.Type) {
				case PresType.Available:
					HandleAvailablePresence (pres);
					break;
				case PresType.Unavailable:
					HandleUnavailablePresence (pres);
					break;
				case PresType.Error:
					HandlePresenceError (pres.Error);
					break;
				default:
					Log.Warn ("Invalid presence received from room " + UID);
					break;
			}
			
			return true;
		}
		
		private void HandleAvailablePresence (Presence pres)
		{
			RoomContact contact;
			if (!_contacts.TryGetValue (pres.From.Resource, out contact)) {
				AddContact (pres);
			}
			else {
				var orig_aff = contact.Affiliation;
				var orig_role = contact.Role;
				var orig_status = contact.Status;
				var orig_dest = contact.StatusDescription;
				
				UpdateContact (contact, pres);
				
				if (orig_aff != contact.Affiliation)
					OnContactAffiliationChanged (contact, null, null); // TODO: actor and reason
				if (orig_role != contact.Role)
					OnContactRoleChanged (contact, null, null);
				if (orig_status != contact.Status || orig_dest != contact.StatusDescription)
					OnContactPresenceChanged (contact);
			}
		}
		
		private void AddContact (Presence pres)
		{
			var status_codes = GetStatusCodes (pres);
			var self_presence = status_codes.BinarySearch (StatusCode.OwnPresence) >= 0;
			
			var contact = new RoomContact ();
			contact.RoomUID = pres.From;
			contact.IsSelfContact = self_presence;
			_contacts.Add (contact.Nickname, contact);
			UpdateContact (contact, pres);
			OnContactAdded (contact);
			
			if (self_presence) {
				_nickname = contact.Nickname;
				
				var non_anonymous = status_codes.BinarySearch (StatusCode.NonAnonymous) >= 0;
				var assigned = status_codes.BinarySearch (StatusCode.NickAssigned) >= 0;
				var logged = status_codes.BinarySearch (StatusCode.LoggingEnabled) >= 0;
				var created = status_codes.BinarySearch (StatusCode.RoomCreated) >= 0;
				
				OnJoined (contact.Nickname, assigned, logged, non_anonymous, created);
				_last_received = null;
				_last_status_codes = null;
			}
			else {
				_last_received = contact;
				_last_status_codes = status_codes;
			}
		}
		
		private void HandleUnavailablePresence (Presence pres)
		{
			RoomContact contact;
			if (_contacts.TryGetValue (pres.From.Resource, out contact)) {
				UpdateContact (contact, pres);
				
				var codes = GetStatusCodes (pres);
				
				if (codes.BinarySearch (StatusCode.NicknameChanged) < 0) {
					if (contact.IsSelfContact) {
						HandleSelfExit (pres, codes);
					}
					else {
						contact.Status = Status.Offline;
						contact.StatusDescription = pres.Status;
						OnContactPresenceChanged (contact);
						OnContactRemoved (contact);
						_contacts.Remove (pres.From.Resource);
						// TODO: handle kicking/banning
					}
				}
				else {
					var old_nick = pres.From.Resource;
					_contacts.Remove (old_nick);
					_contacts.Add (contact.Nickname, contact);
					if (contact.IsSelfContact)
						_nickname = contact.Nickname;
					OnContactNicknameChanged (contact, old_nick);
				}
			}
		}
		
		private void HandleSelfExit (Presence pres, List<StatusCode> codes)
		{
			var x = pres.FirstChild ("x", Namespaces.MucUser);
			var item = x.FirstChild ("item", null);
			
			var act_elm = item.FirstChild ("actor", null);
			var actor = act_elm == null ? null : (JabberID) act_elm ["jid"];
			var reason = item.GetTextChild ("reason", null);
			
			if (codes.BinarySearch (StatusCode.Banned) >= 0) {
				OnExited (MucExitReason.Banned, actor, reason);
				return;
			}
			if (codes.BinarySearch (StatusCode.Kicked) >= 0) {
				OnExited (MucExitReason.Kicked, actor, reason);
				return;
			}
			if (codes.BinarySearch (StatusCode.AffiliationRemovalReason) >= 0) {
				OnExited (MucExitReason.AffiliationChange, null, null);
				return;
			}
			if (codes.BinarySearch (StatusCode.RemovedAsNonMember) >= 0) {
				OnExited (MucExitReason.IsntMember, null, null);
				return;
			}
			if (codes.BinarySearch (StatusCode.SystemShutdown) >= 0) {
				OnExited (MucExitReason.SystemShutdown, null, null);
				return;
			}
			
			OnExited (MucExitReason.Left, null, pres.Status);
		}
		
		public void InviteUser (JabberID uid)
		{
			throw new NotImplementedException ();
			// TODO: invitations, converting one2one chats to MUC 
		}
		
		public void RequestVoice ()
		{
			throw new NotImplementedException ();
			// TODO: voice requests
		}
		
		public void SetSubject (string subject)
		{
			_client.Send (new XmlMessage (MsgType.GroupChat, UID, null, subject, null));
		}
		
		private void UpdateContact (RoomContact contact, Presence pres)
		{
			contact.Status = StatusMethods.GetPresenceStatus (pres);
			contact.StatusDescription = pres.Status;
			
			var x = pres.FirstChild ("x", Namespaces.MucUser);
			if (x == null) return;
			var item = x.FirstChild ("item", null);
			if (item == null) return;
			
			contact.UID = (JabberID) item ["jid"];
			
			var new_nick = item ["nick"];
			if (new_nick != null)
				contact.RoomUID = new JabberID (UID.Node, UID.Domain, new_nick);
			
			try {
				contact.Affiliation = (RoomAffiliation)
					Enum.Parse (typeof (RoomAffiliation), item ["affiliation"]);
			} catch {}
			
			try {
				contact.Role = (RoomRole) Enum.Parse (typeof (RoomRole), item ["role"]);
			} catch {}
		}
		
		private void HandlePresenceError (StanzaError error)
		{
			if (!_joining) {
				OnErrorReceived (error);
				return;
			}
			
			JoinFailureReason reason;
			switch (error.Condition) {
				case "jid-malformed":         reason = JoinFailureReason.NoNickname; break;
				case "not-authorized":
					var pass = RequestPassword ();
					if (pass != null) {
						_client.DetachMessageListener (this);
						_client.DetachPresenceListener (this);
						Join (null, pass, _restrictions);
						return;
					}
					reason = JoinFailureReason.InvalidPassword; break;
				case "registration-required": reason = JoinFailureReason.MembersOnly; break;
				case "forbidden":             reason = JoinFailureReason.Banned; break;
				case "conflict":              reason = JoinFailureReason.NicknameConflict; break;
				case "service-unavailable":   reason = JoinFailureReason.OccupantNumberLimit; break;
				case "item-not-found":        reason = JoinFailureReason.LockedRoom; break;
				default:                      reason = JoinFailureReason.Other; break;
			}
			OnJoinFailed (reason, error);
		}
		
		private string GetRegisteredNickname ()
		{
			var block = new System.Threading.ManualResetEvent (false);
			var password = (string) null;
			var request = new InfoRequest (_client, UID, "x-roomuser-item");
			request.ReceivedEvent += delegate {
				foreach (var ident in request.Result.Identities) {
					password = ident.Name;
					break;
				}
				block.Set ();
			};
			request.ErrorEvent += (sender, e) => block.Set ();
			request.Request ();
			block.WaitOne ();
			return password;
		}

		
		private string RequestNickname ()
		{
			if (NicknameRequested == null) return null;

			var args = new NicknameRequestEventArgs ();
			args.Nickname = _nickname;
			NicknameRequested (this, args);
			return args.Nickname;
		}
		
		private string RequestPassword ()
		{
			if (PasswordRequested == null) return null;
			
			var args = new PasswordRequestEventArgs ();
			PasswordRequested (this, args);
			return args.Password;
		}
		
		private List<StatusCode> GetStatusCodes (Stanza stanza)
		{
			ThrowUtility.ThrowIfNull ("stanza", stanza);
			var list = new List<StatusCode> ();
			var x = stanza.FirstChild ("x", Namespaces.MucUser);
			if (x != null) {
				foreach (var child in x.EachChild ("status")) {
					try { list.Add ((StatusCode) Int32.Parse (child ["code"])); }
					catch {}
				}
				list.Sort ();
			}
			return list;
		}		
		
		public void SendMessage (string message)
		{
			_client.Send (new XmlMessage (MsgType.GroupChat, UID, null, null, message));
		}
		
		public void SendPrivateMessage (string nickname, string message)
		{
			var uid = new JabberID (UID.Node, UID.Domain, nickname);
			_client.Send (new XmlMessage (MsgType.Chat, uid, null, null, message));
		}
		
		#region event emitors
		
		private void OnSubjectChanged (string nick, JabberID source, 
		                               DateTime stamp, string subject)
		{
			if (SubjectChanged != null)
				SubjectChanged (this, new MucSubjectEventArgs (nick, source, stamp, subject));
		}
		
		private void OnMessageReceived (string nick, JabberID source, DateTime stamp,
		                                string thread, string body, bool is_history, bool is_private)
		{
			if (MessageReceived != null) {
				MessageReceived (this, new MucMessageEventArgs (nick, source, stamp, thread,
				                                                body, is_history, is_private));
			}
		}
		
		private void OnContactAdded (RoomContact contact)
		{
			if (ContactAdded != null)
				ContactAdded (this, new RoomContactEventArgs (contact));
		}
		
		private void OnContactRemoved (RoomContact contact)
		{
			if (ContactRemoved != null)
				ContactRemoved (this, new RoomContactEventArgs (contact));
		}
		
		private void OnContactPresenceChanged (RoomContact contact)
		{
			if (ContactPresenceChanged != null)
				ContactPresenceChanged (this, new RoomContactEventArgs (contact));
		}
		
		private void OnContactAffiliationChanged (RoomContact contact, JabberID actor, string reason)
		{
			if (ContactAffiliationChanged != null)
				ContactAffiliationChanged (this, new RoomContactEventArgs (contact));
		}
		
		private void OnContactRoleChanged (RoomContact contact, JabberID actor, string reason)
		{
			if (ContactRoleChanged != null)
				ContactRoleChanged (this, new RoomContactEventArgs (contact));
		}
		
		private void OnJoined (string nick, bool assigned, bool logged,
		                       bool non_anonymous, bool created)
		{
			_joining = false;
			Connected = true;
			if (Joined != null)
				Joined (this, new JoinEventArgs (nick, assigned, logged, non_anonymous, created));
		}
		
		private void OnJoinFailed (JoinFailureReason reason, StanzaError error)
		{
			_client.DetachMessageListener (this);
			_client.DetachPresenceListener (this);
			
			if (JoinFailed != null)
				JoinFailed (this, new JoinFailedEventArgs (reason, error));
		}
		
		private void OnExited (MucExitReason reason, JabberID actor, string act_reason)
		{
			_client.DetachMessageListener (this);
			_client.DetachPresenceListener (this);
			Connected = false;			
			
			if (Exited != null)
				Exited (this, new MucExitEventArgs (reason, actor, act_reason));
			
			_contacts.Clear ();
		}
		
		private void OnContactNicknameChanged (RoomContact contact, string old_name)
		{
			if (ContactNicknameChanged != null)
				ContactNicknameChanged (this, new RoomContactNicknameEventArgs (contact, old_name));
		}
		
		private void OnErrorReceived (StanzaError error)
		{
			if (ErrorReceived != null)
				ErrorReceived (this, new MucErrorEventArgs (error.GetHumanRepresentation ()));
		}
		
		#endregion
	}
}
